import {
  Injectable,
  ConflictException,
  NotFoundException,
  BadRequestException,
  Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Device } from './entities/device.entity';
import { DeviceStatus } from '../common/enums';
import {
  CreateDeviceDto,
  BindDeviceDto,
  UpdateDeviceStatusDto,
  UpdateDeviceMetadataDto,
  DeviceMetaDto,
} from './dto';
import { DeviceAdminModel } from './models';
import { NotifyService } from './notify.service';
import { UserRole } from '../common/enums';

@Injectable()
export class DevicesService {
  private readonly logger = new Logger(DevicesService.name);

  constructor(
    @InjectRepository(Device)
    private deviceRepository: Repository<Device>,
    private notifyService: NotifyService,
  ) {}

  /**
   * Map Device entity to DeviceAdminModel
   */
  private mapToDeviceAdminModel(device: Device): DeviceAdminModel {
    // Ensure metadata has a version, fallback to "unknown" if not present
    const metadata = {
      ...(device.metadata || {}),
      version: device.metadata?.version || 'unknown',
    };

    return {
      id: device.id,
      serialNumber: device.serialNumber,
      status: device.status,
      registeredBy: device.registeredBy,
      registeredAt: device.registeredAt,
      lastUsedAt: device.lastUsedAt,
      userId: device.userId,
      metadata: metadata as DeviceMetaDto,
    };
  }

  /**
   * Register a new device
   */
  async registerDevice(
    createDeviceDto: CreateDeviceDto,
    adminId: string,
  ): Promise<DeviceAdminModel> {
    const { serialNumber, metadata } = createDeviceDto;

    // Check if serial number already exists
    const existingDevice = await this.deviceRepository.findOne({
      where: { serialNumber },
    });

    if (existingDevice) {
      throw new ConflictException(`Device with serial number ${serialNumber} already exists`);
    }

    // Create new device with version added to metadata
    const deviceMetadata = {
      ...(metadata || {}),
      version: '1', // Add version to metadata in the database
    };

    const device = this.deviceRepository.create({
      serialNumber,
      status: DeviceStatus.ACTIVE,
      registeredBy: adminId,
      registeredAt: new Date(),
      metadata: deviceMetadata,
    });

    const savedDevice = await this.deviceRepository.save(device);
    this.logger.log(`Device ${serialNumber} registered by admin ${adminId}`);

    return this.mapToDeviceAdminModel(savedDevice);
  }

  /**
   * Bind a device to a user
   */
  async bindDeviceToUser(
    deviceId: string,
    bindDeviceDto: BindDeviceDto,
    adminId: string,
  ): Promise<DeviceAdminModel> {
    const { userId } = bindDeviceDto;

    // Find device
    const device = await this.deviceRepository.findOne({ where: { id: deviceId } });
    if (!device) {
      throw new NotFoundException(`Device with ID ${deviceId} not found`);
    }

    // Update device with new user
    device.userId = userId;
    const updatedDevice = await this.deviceRepository.save(device);

    this.logger.log(`Device ${deviceId} bound to user ${userId} by admin ${adminId}`);

    return this.mapToDeviceAdminModel(updatedDevice);
  }

  /**
   * Update device status
   */
  async updateDeviceStatus(
    deviceId: string,
    updateStatusDto: UpdateDeviceStatusDto,
    adminId: string,
  ): Promise<DeviceAdminModel> {
    const { status, remark } = updateStatusDto;

    // Find device
    const device = await this.deviceRepository.findOne({ where: { id: deviceId } });
    if (!device) {
      throw new NotFoundException(`Device with ID ${deviceId} not found`);
    }

    const currentStatus = device.status;

    // Validate status transition
    this.validateStatusTransition(currentStatus, status, remark);

    // Update device status
    device.status = status;

    // If status is LOST or RETIRED, unbind user
    if (status === DeviceStatus.LOST || status === DeviceStatus.RETIRED) {
      if (device.userId) {
        const userId = device.userId;
        device.userId = null;

        // Notify user about device status change
        await this.notifyService.notifyUserAboutDeviceStatusChange(
          userId,
          deviceId,
          device.serialNumber,
          status,
          remark,
        );
      }
    }

    const updatedDevice = await this.deviceRepository.save(device);

    this.logger.log(
      `Device ${deviceId} status changed from ${currentStatus} to ${status} by admin ${adminId}`,
    );

    return this.mapToDeviceAdminModel(updatedDevice);
  }

  /**
   * Update device metadata
   */
  async updateDeviceMetadata(
    deviceId: string,
    updateMetadataDto: UpdateDeviceMetadataDto,
    adminId: string,
  ): Promise<DeviceAdminModel> {
    const { metadata } = updateMetadataDto;

    // Find device
    const device = await this.deviceRepository.findOne({ where: { id: deviceId } });
    if (!device) {
      throw new NotFoundException(`Device with ID ${deviceId} not found`);
    }

    // Merge metadata
    device.metadata = {
      ...(device.metadata || {}),
      ...metadata,
    };

    const updatedDevice = await this.deviceRepository.save(device);

    this.logger.log(`Device ${deviceId} metadata updated by admin ${adminId}`);

    return this.mapToDeviceAdminModel(updatedDevice);
  }

  /**
   * Validate device status transition
   */
  private validateStatusTransition(
    currentStatus: DeviceStatus,
    newStatus: DeviceStatus,
    remark?: string,
  ): void {
    // Define allowed transitions
    const allowedTransitions = {
      [DeviceStatus.ACTIVE]: [DeviceStatus.LOST, DeviceStatus.RETIRED],
      [DeviceStatus.LOST]: [DeviceStatus.ACTIVE, DeviceStatus.RETIRED],
      // No transitions from RETIRED (end of lifecycle)
      [DeviceStatus.RETIRED]: [],
      // For completeness, though not in the state diagram
      [DeviceStatus.DISABLED]: [DeviceStatus.ACTIVE],
    };

    // Check if transition is allowed
    if (!allowedTransitions[currentStatus].includes(newStatus)) {
      throw new BadRequestException(
        `Invalid status transition from ${currentStatus} to ${newStatus}`,
      );
    }

    // Check if remark is provided for LOST or RETIRED status
    if (
      (newStatus === DeviceStatus.LOST || newStatus === DeviceStatus.RETIRED) &&
      (!remark || remark.trim() === '')
    ) {
      throw new BadRequestException(`Remark is required when changing status to ${newStatus}`);
    }
  }

  /**
   * Find device by serial number
   * @param serialNumber Device serial number
   * @returns Device or null if not found
   */
  async findBySerialNumber(serialNumber: string): Promise<Device | null> {
    return this.deviceRepository.findOne({ where: { serialNumber } });
  }

  /**
   * Update a device
   * @param deviceId Device ID
   * @param updateData Update data
   * @returns Updated device
   */
  async update(deviceId: string, updateData: any): Promise<Device> {
    await this.deviceRepository.update(deviceId, updateData);
    return this.getDeviceById(deviceId);
  }

  /**
   * Get device by ID
   */
  async getDeviceById(deviceId: string): Promise<Device> {
    const device = await this.deviceRepository.findOne({ where: { id: deviceId } });
    if (!device) {
      throw new NotFoundException(`Device with ID ${deviceId} not found`);
    }
    return device;
  }

  /**
   * Find all devices with filtering options
   */
  async findAll(options: {
    status?: DeviceStatus;
    userId?: string;
    page: number;
    limit: number;
  }): Promise<{ items: DeviceAdminModel[]; meta: { total: number; page: number; limit: number } }> {
    const { status, userId, page, limit } = options;

    // Build where clause
    const whereOptions: any = {};

    if (status) {
      whereOptions.status = status;
    }

    if (userId) {
      whereOptions.userId = userId;
    }

    // Calculate skip for pagination
    const skip = (page - 1) * limit;

    // Get devices with pagination
    const [devices, total] = await this.deviceRepository.findAndCount({
      where: whereOptions,
      skip,
      take: limit,
      order: { registeredAt: 'DESC' },
    });

    return {
      items: devices.map((device) => this.mapToDeviceAdminModel(device)),
      meta: {
        total,
        page,
        limit,
      },
    };
  }

  /**
   * Get device details by ID
   */
  async getDeviceDetails(
    deviceId: string,
    userRole: UserRole,
    userId: string,
  ): Promise<DeviceAdminModel> {
    const device = await this.getDeviceById(deviceId);

    // If user is not admin, check if they own the device
    if (userRole !== UserRole.ADMIN && device.userId !== userId) {
      throw new NotFoundException(`Device with ID ${deviceId} not found`);
    }

    return this.mapToDeviceAdminModel(device);
  }
}
